﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Niknak.Services.ServiceCore.TaskScheduler
{
    /// <summary>
    /// Provides the ability to perform a task according to a specified schedule
    /// </summary>
    /// <typeparam name="ConfigType">Class containing scheduling information</typeparam>
    public abstract class SimpleTaskProcessor<ConfigType> where ConfigType : ISimpleTaskConfig, new()
    {
        private Timer _Timer = null;
        private TimeSpan _StartTime;
        private TimeSpan _Interval;
        private DateTime _CurrentExecutingIteration;
        /// <summary>
        /// Allows for maintaining a flag showing if the task is currently running.
        /// </summary>
        /// <remarks>
        /// Set to true at the start of each iteration (e.g. top of ProcessTask), false at the end
        /// </remarks>
        protected bool _TaskInProgress = false;
        /// <summary>
        /// Allows for maintaining a flag showing if the processing engine is shutting down to avoid starting a new iteration
        /// </summary>
        protected /*static*/ bool _ShuttingDown = false;
        /// <summary>
        /// Delegate method required to run code on each iteration
        /// </summary>
        protected ProcessTask _TaskProcessor;

        protected ConfigType _Settings;
        /// <summary>
        /// Object which contains implementation of ISimpleTaskConfig
        /// </summary>
        public ConfigType Settings
        {
            get
            {
                if (_Settings == null)
                    _Settings = new ConfigType();
                return _Settings;
            }
            set { _Settings = value; }
        }

        /// <summary>
        /// Uses the supplied config information to calculate the target start date for the task
        /// </summary>
        /// <remarks>
        /// Typically a SimpleTaskProcessor is used on tasks that need to run for an indefinite number of iterations, so a start date is rarely required.
        /// However, when used, it's possible to delay the first iteration until the combined start date and start time. Thereafter, start date is ignored.
        /// </remarks>
        public DateTime CalculateTargetStart()
        {
            DateTime targetStart = DateTime.MinValue;

            //ideally the target start is a combination of the start date and the start time
            if (!DateTime.TryParseExact(Settings.StartDate + " " + Settings.StartTime, "yyyy-MM-dd HH:mm:ss", null, System.Globalization.DateTimeStyles.AssumeUniversal, out targetStart))
            {
                targetStart = DateTime.MinValue;

                if (!string.IsNullOrEmpty(Settings.StartDate))
                {
                    string[] startDateParts = Settings.StartDate.Split('-');
                    if (!startDateParts.Length.Equals(3))
                        throw new Exception("Invalid scheduled task start date detected; use the format yyyy-MM-dd");
                }

                string[] startTimeParts = Settings.StartTime.Split(':');
                if (!startTimeParts.Length.Equals(3))
                    throw new Exception("Invalid scheduled task start time detected; use the format d.HH:mm:ss");

                _StartTime = TimeSpan.Parse(Settings.StartTime);
                targetStart = DateTime.UtcNow.Date.Add(_StartTime);
            }
            else
                targetStart = targetStart.ToUniversalTime();

            //make sure the start date is on an allowed day
            while (!IsValidIterationDay(Settings, targetStart))
                targetStart = targetStart.AddDays(1);

            return targetStart;
        }

        private static bool IsValidIterationDay(ConfigType config, DateTime forIterationDateTime)
        {
            if (config.RunOnDays != InternalEnums.TaskScheduler.DaysOfWeek.Unknown && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.All))
            {
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Monday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Monday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Tuesday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Tuesday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Wednesday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Wednesday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Thursday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Thursday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Friday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Friday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Saturday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Saturday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Sunday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Sunday))
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Starts the scheduler and runs ProcessTask at the configured interval.
        /// </summary>
        public virtual void Initialise()
        {
            if (_TaskProcessor == null)
                throw new Exception("Task processor delegate is undefined");

            RestartTimer();
        }

        private void RestartTimer()
        {
            if (_Timer != null)
                _Timer.Dispose();

            if (_ShuttingDown)
                return;

            DateTime targetStart = CalculateTargetStart();
            string[] intervalParts = Settings.Interval.Split(':');
            
            if (!intervalParts.Length.Equals(3))
                throw new Exception("Invalid scheduled task interval detected; use the format d.HH:mm:ss");

            _Interval = TimeSpan.Parse(Settings.Interval);

            //while the current time is ahead of the target start, add intervals until the target start is in the future
            while (DateTime.UtcNow >= targetStart)
                targetStart = targetStart.Add(_Interval);

            //calculate the time between now and the first iteration - this is how long to wait before the first timer callback
            TimeSpan initialDue = targetStart.Subtract(DateTime.UtcNow);

            if (initialDue.TotalMilliseconds > UInt32.MaxValue)
                throw new Exception("The initial due time of the task is too far in the future and exceeds UInt32.MaxValue (i.e. ~49 days). Specified initial due time was ~" + ((int)initialDue.TotalDays).ToString() + " days.");
            if (_Interval.TotalMilliseconds > UInt32.MaxValue)
                throw new Exception("The interval for the task is too large and exceeds UInt32.MaxValue (i.e. ~49 days). Specified interval was ~" + ((int)_Interval.TotalDays).ToString() + " days.");

            //if we're a few ms behind for some reason, correct to start immediately
            if (initialDue.TotalMilliseconds < 0D)
                initialDue = TimeSpan.FromMilliseconds(0D);

            LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Information, "First iteration scheduled to run in " + initialDue.ToString() + " at " + targetStart.ToString());

            _Timer = new Timer(
                new TimerCallback(ValidateAndRunIteration),
                Settings,
                initialDue,
                _Interval
            );
        }

        /// <summary>
        /// Shuts down the task processor by setting _ShuttingDown to true and disposing the internal timer
        /// </summary>
        public void Shutdown()
        {
            _ShuttingDown = true;
            if (TaskShuttingDown != null)
                try
                {
                    TaskShuttingDown(this, EventArgs.Empty);
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in TaskShuttingDown event: " + ex.Message);
                }
            if (_Timer != null)
                _Timer.Dispose();
        }

        /// <summary>
        /// Called on every task iteration to check that this iteration falls within the acceptible time range to run
        /// </summary>
        /// <param name="obj">State object from the timer (implements IAdvancedTaskConfig)</param>
        protected void ValidateAndRunIteration(object obj)
        {
            if (_Timer != null)
                _Timer.Dispose();

            if (_ShuttingDown)
                return;

            //is this task still running? if so get out now
            if (_TaskInProgress)
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Warning, "Unable to start task iteration because a previous task iteration is still running");
                return;
            }

            ConfigType config = (ConfigType)obj;
            _CurrentExecutingIteration = DateTime.UtcNow;

            //not running on a valid day? bye bye
            if (!IsValidIterationDay(config, _CurrentExecutingIteration))
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Warning, "Unable to start task iteration because the current DateTime of " + DateTime.UtcNow.ToString() + " does not fall on one of the days this task is allowed to run");
                return;
            }

            if (IterationStarting != null)
                try
                {
                    IterationStarting(this, new IterationEventArgs(config, _CurrentExecutingIteration));
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Iteration aborted. Exception raised in IterationStarting event: " + ex.Message);
                    _TaskInProgress = false;
                    _CurrentExecutingIteration = DateTime.MinValue;
                    return;
                }

            _TaskInProgress = true;
            try
            {
                _TaskProcessor(obj);
            }
            catch (Exception ex)
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in ProcessTask delegate: " + ex.Message);
            }
            _TaskInProgress = false;

            if (IterationCompleted != null)
                try
                {
                    IterationCompleted(this, new IterationEventArgs(config, _CurrentExecutingIteration));
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in IterationCompleted event: " + ex.Message);
                }

            //reset the timer to help with drift
            RestartTimer();

            _CurrentExecutingIteration = DateTime.MinValue;
            LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Information, "ValidateAndRunIteration has completed");
        }

        /// <summary>
        /// Allows for the completed event to be raised after the iteration runs. Useful when the completed event has been disabled or failed.
        /// </summary>
        /// <param name="iterationDate">Date of the iteration for context</param>
        public void RaiseIterationCompletedEvent(DateTime iterationDate)
        {
            if (IterationCompleted != null)
                try
                {
                    IterationEventArgs args = new IterationEventArgs(Settings, iterationDate);
                    args.EnableOverrideFlag();

                    IterationCompleted(this, args);
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in IterationCompleted event: " + ex.Message);
                }
        }

        /// <summary>
        /// Raised before the task iteration begins
        /// </summary>
        public event EventHandler<IterationEventArgs> IterationStarting;

        /// <summary>
        /// Raised when the task iteration ends
        /// </summary>
        public event EventHandler<IterationEventArgs> IterationCompleted;

        /// <summary>
        /// Raised when the task is beginning its shutdown routine
        /// </summary>
        public event EventHandler TaskShuttingDown;

        /// <summary>
        /// Raised when a logging message is generated
        /// </summary>
        public event EventHandler<LoggingEventArgs> LogMessageGenerated;

        private void LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity severity, string message)
        {
            LogMessage(severity, message, null);
        }

        private void LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity severity, string message, Exception exception)
        {
            if (LogMessageGenerated != null)
                try
                {
                    LogMessageGenerated(this, new LoggingEventArgs(Settings, _CurrentExecutingIteration, severity, message, exception));
                }
                catch { }
        }

        /// <summary>
        /// Method to call on each timer iteration
        /// </summary>
        /// <param name="obj">State object which is a copy of the Settings property</param>
        protected delegate void ProcessTask(object obj);
    }
}
